/*
 * Copyright 2009 BioTeam Inc
 * 
 * Licensed under License GNU LESSER GENERAL PUBLIC LICENSE version 3.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.gnu.org/copyleft/lesser.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package net.bioteam.appweb.datatype;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import net.bioteam.appweb.Config;
import net.bioteam.appweb.model.Program;
import net.bioteam.appweb.model.ProgramBuilder;

import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;


public class ParameterMapBuilder
{
	public static final Log logger=LogFactory.getLog(ParameterMapBuilder.class);

	/**
	 *filekey<->parameterMap 
	 */
	public static Map<String, ParameterMap> filekeyMaps=new HashMap<String, ParameterMap>();
	
	/**
	 * input mobytype->parameteMap  
	 */
	public static Map<String, List<ParameterMap>> inputMaps=new HashMap<String, List<ParameterMap>>();
	
	/**
	 * programName->parameterMap
	 */
	public static Map<String, List<ParameterMap>> programMaps=new HashMap<String, List<ParameterMap>>();
	
	/*
	 * types mentioned in all type files.
	 */
	public static Map<String, SimpleDataType> typeToNode=new HashMap<String, SimpleDataType>();
	
	public static SimpleDataType usedTypes;
	
	public static String usedTypesJSON;
	
	
	/**
	 * find a type node from usedType tree.
	 * @param name
	 * @return
	 */
	public static SimpleDataType findType(String name)
	{
		return findDescedent(usedTypes, name);
	}
	
	private static SimpleDataType findDescedent(SimpleDataType t, String name)
	{
		if (t.getName().equals(name))
			return t;
		for (SimpleDataType child: t.getChildren())
		{
			SimpleDataType found=findDescedent(child, name);
			if (found!=null)
				return found;
		}
		return null;
	}
	
	public static boolean isParentType(String parent, String child)
	{
		SimpleDataType childNode=typeToNode.get(child);
		return hasParent(childNode, parent);
	}
	
	private static boolean hasParent(SimpleDataType node, String parent)
	{
		if (node.getName().equals(parent))
		{
			return true;
		}
		if (node.getParent()==null)
		{
			return false;
		}
		return hasParent(node.getParent(), parent);
	}
	
	
	
	public static List<String> findTypeAndParentType(String type)
	{
		List<String> ret=new LinkedList<String>();
		SimpleDataType node=typeToNode.get(type);
		if (node!=null)
		{
			collectTypeAndParentType(node, ret);
		}
		
		return ret;
	}
	
	private static void collectTypeAndParentType(SimpleDataType node, List<String> types)
	{
		if (node.getParent()!=null)
		{
			collectTypeAndParentType(node.getParent(), types);
		}
		types.add(node.getName());
		
	
	}
	
	public static Set<ParameterMap> findMapsByInputs(List<String> types)
	{
		Set<ParameterMap> maps=new HashSet<ParameterMap>();
		for (String type: types)
		{
			List<ParameterMap> m=inputMaps.get(type);
			if (m!=null)
			{
				maps.addAll(m);
			}
		}
		return maps;
	}
	
	
	private static SimpleDataType loadTypeCache() throws Exception
	{
//		MobyTypeCache.worker = new CentralDigestCachedImpl(CentralImpl.DEFAULT_ENDPOINT, CentralImpl.DEFAULT_NAMESPACE, FilenameUtils.separatorsToSystem(Config.topdir+File.separator+"mobycache"));
//		MobyTypeCache.init();
//
//		DataTypeNode root = MobyTypeCache.rootNode;
//		// object node
//		DataTypeNode object = root.getSubTypes().get(0);
//		SimpleDataType objectS = toSimpleType(object);
//		return objectS;
		XMLDecoder d=new XMLDecoder(new BufferedInputStream(new FileInputStream(Config.topdir+File.separator+"mobycache"+File.separator+"types.xml")));
		SimpleDataType objectType=(SimpleDataType)d.readObject();
		d.close();
		return objectType;
	}
	
	public static void init() throws Exception
	{
		SimpleDataType objectType=loadTypeCache();
		loadAllMap();
		constructUsedTypeTree(objectType);
		JSONArray ja = new JSONArray();
		for (SimpleDataType childNode : usedTypes.getChildren())
		{
			ja.put(dataNodeToJSON(childNode));
		}
		usedTypesJSON=ja.toString();
	}
	private static void constructUsedTypeTree(SimpleDataType objectType)
	{
		Map<String, Boolean> touchedData=new HashMap<String, Boolean>();
		for (ParameterMap map:filekeyMaps.values())
		{
			for (Input match: map.getInputs())
			{
				touchedData.put(match.getMobyTypeName(), true);
			}
			for (Output match: map.getOutputs())
			{
				touchedData.put(match.getMobyTypeName(), true);
			}
		}
		isNodeTouched(objectType, touchedData);
		
		usedTypes=objectType;
	}
	
	/**
	 * 
	 * @param node
	 * @param touchedData
	 * @return whether this node or one of its children node is touched
	 */
	private static boolean isNodeTouched(SimpleDataType node, Map<String, Boolean> touchedData)
	{
		
		for (ListIterator<SimpleDataType> iter=node.getChildren().listIterator(); iter.hasNext();)
		{
			SimpleDataType childNode=iter.next();
			if (!isNodeTouched(childNode, touchedData))
			{
				iter.remove();
			}
		}
		if (touchedData.containsKey(node.getName()))
		{
			typeToNode.put(node.getName(), node);
			//this node is touched.
			return true;
		}
		if (node.getChildren().isEmpty())
		{
			//none of the children node is used.
			return false;
		} else
		{
			//some of the chidren type is used.
			return true;
		}
	}
	

	private static void loadAllMap() throws JAXBException
	{
		File dir=new File(Config.PARAMETER_MAP_DIR);
		if (!dir.exists())
		{
			dir.mkdirs();
		}
		JAXBContext jc = JAXBContext.newInstance("net.bioteam.appweb.datatype");
		Unmarshaller unmarshaller = jc.createUnmarshaller();
		unmarshaller.setProperty("com.sun.xml.bind.ObjectFactory",new ObjectFactory());
		for (File file: dir.listFiles((FileFilter)new SuffixFileFilter(".xml")))
		{
			//logger.info("loading parameter map for "+file.getAbsolutePath());
			String filekey=file.getName().substring(0, file.getName().length()-4);
			try
			{
				Object obj = unmarshaller.unmarshal(file);
				JAXBElement<_ParameterMap> jcmd = (JAXBElement<_ParameterMap>) obj;
				ParameterMap map=(ParameterMap)jcmd.getValue();
				Program p=null;
				try
				{
					p = ProgramBuilder.getProgram(map.getProgram());
				} catch (Exception e)
				{
					logger.warn("fail to get program", e);
				}
				if (p==null)
				{
					//do not load map for application that does not exist.
					continue;
				}
				map.setFilekey(filekey);
				filekeyMaps.put(filekey, map);
			} catch (JAXBException e)
			{
				logger.warn("fail to build parametermap file for "+filekey, e);
			}
		}
		
		
		for (ParameterMap map: filekeyMaps.values())
		{
			for (Input in: map.getInputs())
			{
				if (inputMaps.containsKey(in.getMobyTypeName()))
				{
					inputMaps.get(in.getMobyTypeName()).add(map);
				} else
				{
					List<ParameterMap> maps=new LinkedList<ParameterMap>();
					maps.add(map);
					inputMaps.put(in.getMobyTypeName(), maps);
				}
			}
		}
		
		for (ParameterMap map: filekeyMaps.values())
		{
			if (programMaps.containsKey(map.getProgram()))
			{
				programMaps.get(map.getProgram()).add(map);
			} else
			{
				List<ParameterMap> maps=new LinkedList<ParameterMap>();
				maps.add(map);
				programMaps.put(map.getProgram(), maps);
			}
		}
		
	}
	
	
	
	public static ParameterMap getParameterMap(File file) throws JAXBException
	{
		JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class);
		Unmarshaller unmarshaller = jc.createUnmarshaller();
		unmarshaller.setProperty("com.sun.xml.bind.ObjectFactory",new ObjectFactory());
		if (!file.exists())
		{
			return null;
		}
		
		Object obj = unmarshaller.unmarshal(file);
		JAXBElement<_ParameterMap> jcmd = (JAXBElement<_ParameterMap>) obj;
		ParameterMap map= (ParameterMap)jcmd.getValue();
		return map;
		
	}
	
	
	public static void outputMap(ParameterMap map, OutputStream out) throws JAXBException
	{
		JAXBContext jc = JAXBContext.newInstance("net.bioteam.appweb.datatype");
		Marshaller marshaller=jc.createMarshaller();
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		JAXBElement<_ParameterMap> element=new ObjectFactory().createParameterMap(map);
		marshaller.marshal(element, out);
	}
	
	public static JSONObject dataNodeToJSON(SimpleDataType node) throws JSONException
	{
		JSONObject obj = new JSONObject();
		
			obj.put("text", node.getName());
			obj.put("qtip", node.getDescription());
			if (node.getChildren().isEmpty())
			{
				obj.put("leaf", true);
			} else
			{
				obj.put("leaf", false);
			}
		
		if (!node.getChildren().isEmpty())
		{
			JSONArray array = new JSONArray();
			Collections.sort(node.getChildren(), new Comparator<SimpleDataType>(){

				public int compare(SimpleDataType o1, SimpleDataType o2)
				{
					//since the virtual sequence is at the end, we make the virtual sequence 
					//show at the beginning
					return -o1.getName().compareToIgnoreCase(o2.getName());
				}
				
			});
			for (SimpleDataType childNode : node.getChildren())
			{
				array.put(dataNodeToJSON(childNode));
			}
			obj.put("children", array);
		}
		return obj;
	}

	private static SimpleDataType toSimpleType(DataTypeNode node)
	{
		SimpleDataType ret = new SimpleDataType();
		ret.setName(node.type.getName());
		ret.setDescription(node.type.getDescription());
		for (DataTypeNode child : node.getSubTypes())
		{
			SimpleDataType childS = toSimpleType(child);
			childS.setParent(ret);
			ret.getChildren().add(childS);
		}
		return ret;
	}
}
